﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Configuration;
using System.Dynamic;
using Platform.Storage.Redis.Hash;
using Platform.Storage.Redis.Messages;

namespace Platform.Storage.Redis
{

	/// <summary>
	/// Representa um conexão com o REDIS
	/// </summary>
	public sealed class RedisConnection : IDisposable
	{
		//private static readonly TimeSpan KeepAliveTimeout = TimeSpan.FromSeconds( 30 );
		private object openingSync = new object();
		private Socket connection;


		public RedisConnection( string csName )
			: this( ( Uri )null )
		{
			var cs = ConfigurationManager.ConnectionStrings[ csName ];

			if ( cs == null )
				throw new ArgumentException( String.Format( "A string de conexão \"{0]\" não foi encontrado no arquivo de configuração da aplicação.", csName ) );

			this.Url = new Uri( cs.ConnectionString );
		}

		public RedisConnection( Uri server )
		{
			this.Url = server ?? new Uri( "tcp://localhost:6379" );
			this.KeepAlive = true;
			this.KeepAliveTimeout = TimeSpan.FromSeconds( 30 );
			this.IOTimeout = TimeSpan.FromSeconds( 30 );
			this.Channel = new RedisMessageChannel();
			this.Keys = new RedisKeysCommands( this.Channel );
			this.Server = new RedisServerCommands( this.Channel );
		}

		/// <summary>
		/// Obtém o endereço do servidor REDIS que está sendo acessado
		/// </summary>
		public Uri Url
		{ get; private set; }

		/// <summary>
		/// Obtém ou define o tempo de espera para enviar e receber os comandos enviados para o REDIS
		/// </summary>
		public TimeSpan IOTimeout
		{ get; set; }

		/// <summary>
		/// Se true a conexão com o servidor é mantida ativa
		/// </summary>
		public bool KeepAlive
		{ get; set; }

		/// <summary>
		/// Obtém ou define o intervalo de tempo que será verificado se a conexão ainda está ativa
		/// </summary>
		public TimeSpan KeepAliveTimeout
		{ get; set; }

		/// <summary>
		/// Verifica se está conectado
		/// </summary>
		public bool IsConnected
		{ get { return this.connection != null && this.connection.Connected; } }

		/// <summary>
		/// Canal por onde serão enviadas as mensagens para o REDIS
		/// </summary>
		public RedisMessageChannel Channel
		{ get; private set; }

		/// <summary>
		/// 
		/// </summary>
		public RedisKeysCommands Keys
		{ get; private set; }

		/// <summary>
		/// Comandos de administração do servidor REDIS
		/// </summary>
		public RedisServerCommands Server
		{ get; private set; }

		public Messaging SubscribeIfMatches( params string[] channels )
		{
			return this.SubscribeIfMatches( MessagingConnection.OwnsConnection, channels );
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="state"></param>
		/// <param name="channels"></param>
		/// <returns></returns>
		public Messaging SubscribeIfMatches( MessagingConnection state, params string[] channels )
		{
			return this.Subscribe( state, SubscriptionMatch.ByPattern, channels );
		}


		/// <summary>
		/// 
		/// </summary>
		/// <param name="channel"></param>
		/// <returns></returns>
		public Messaging Subscribe( params string[] channels )
		{
			return this.Subscribe( MessagingConnection.OwnsConnection, channels );
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="state"></param>
		/// <param name="channels"></param>
		/// <returns></returns>
		public Messaging Subscribe(MessagingConnection state, params string[] channels)
		{
			return this.Subscribe( state, SubscriptionMatch.Exact, channels );	
		}

		private Messaging Subscribe( MessagingConnection state, SubscriptionMatch match, string[] channels )
		{
			Messaging channel;

			this.KeepAlive = false;
			if ( state == MessagingConnection.OwnsConnection )
				channel = new Messaging( this );
			else
				channel = new Messaging( this.Channel );

			channel.Subscribe( match, channels );
			return channel;
		}

		/// <summary>
		/// Realiza a publicação da mensagem <paramref name="message"/> no canal <paramref name="channel"/>
		/// </summary>
		/// <param name="channel"></param>
		/// <param name="message"></param>
		/// <returns></returns>
		public int Publish( string channel, string message )
		{
			return this.Channel.Send( new PublishMessage( this.Channel, channel, message ) ).Reply;
		}

		/// <summary>
		/// Listas
		/// </summary>
		/// <param name="listName"></param>
		/// <returns></returns>
		public RedisListCommands Lists( string listName )
		{
			return new RedisListCommands( listName, this.Channel );
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="setName"></param>
		/// <returns></returns>
		public RedisSetsCommands Sets( string setName )
		{
			return new RedisSetsCommands( setName, this.Channel );
		}

		/// <summary>
		/// 
		/// </summary>
		/// <typeparam name="THash"></typeparam>
		/// <param name="hashName"></param>
		/// <returns></returns>
		public THash Hash<THash>( string hashName )
			where THash : IHashObject
		{
			return this.Hash<THash>( hashName, SendCommandBehavior.Batch );
		}

		/// <summary>
		/// Obtém a tabela hash
		/// </summary>
		/// <typeparam name="THash"></typeparam>
		/// <param name="hashName"></param>
		/// <param name="behavior"></param>
		/// <returns></returns>
		public THash Hash<THash>( string hashName, SendCommandBehavior behavior )
			where THash : IHashObject
		{
			AnyObject<THash> hash = new AnyObject<THash>( behavior, hashName, this.Channel );
			return ( THash )hash.GetTransparentProxy();
		}

		/// <summary>
		/// Abre a conexão com o servidor REDIS
		/// </summary>
		public void Open()
		{
			this.Open( this.Url );
		}

		/// <summary>
		/// Abre a conexão com o servidor REDIS
		/// </summary>
		/// <param name="redisAddress">Endereço do servidor</param>
		public void Open( Uri redisAddress )
		{
			if ( redisAddress == null ) throw new ArgumentNullException( "redisAddress" );

			lock ( this.openingSync )
			{
				if ( this.IsConnected )
					throw new InvalidOperationException( "A conexão já foi aberta." );

				// Se Open for chamado mais de uma vez, garante que fechou a última conexão
				// this.Close();

				this.connection = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );

				this.connection.Blocking = true;
				this.connection.NoDelay = true;
				//this.connection.EnableBroadcast = false;
				this.connection.SendTimeout = ( int )this.IOTimeout.TotalMilliseconds;
				this.connection.ReceiveTimeout = ( int )this.IOTimeout.TotalMilliseconds;

				//this.Channel = new RedisMessageChannel( this.connection );

				try
				{
					this.connection.Connect( redisAddress.Host, redisAddress.Port );
					this.Channel.ResetConnection( this.connection );

					if ( this.KeepAlive )
					{
						var monitor =
							new Thread(
								new ThreadStart(
									() =>
									{
										//var channelToMonitor = this.Channel;

										while ( this.KeepAlive 
											&& this.KeepAliveTimeout > TimeSpan.Zero 
											&& this.IsConnected )
										{
											// Verifica o período em que está inativo
											// Se for maior, envia um ping para o servidor
											TimeSpan inactivePeriod = DateTime.Now.Subtract( this.Channel.Stats.LastSent );
											if ( inactivePeriod >= KeepAliveTimeout )
												this.Server.Ping();

											Thread.Sleep( KeepAliveTimeout );
										}
									} ) );
						monitor.Start();
					}


					this.Url = redisAddress;
				}
				catch
				{
					this.Close();
					throw;
				}
			}
		}

		/// <summary>
		/// Fecha a conexão com o servidor REDIS
		/// </summary>
		public void Close()
		{
			this.Close( true );
		}

		/// <summary>
		/// Fecha a conexão com o servidor REDIS
		/// </summary>
		/// <param name="quiting">true indica que o comando QUIT deve ser enviado para o servidor antes de fechar o socket</param>
		private void Close( bool quiting )
		{
			if ( this.connection != null )
			{
				if ( quiting )
					this.Server.Quit();

				this.Channel.CloseStreams();

				this.connection.Close();
				this.connection.Dispose();
				this.connection = null;
			}
		}

		#region IDisposable Members

		public void Dispose()
		{
			this.Close();
		}

		#endregion
	}

	
}
